-
Notifications
You must be signed in to change notification settings - Fork 0
feat: Zipkin 분산 추적 및 Order-User 서비스 간 호출 추적 구현 #11
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
- Micrometer Tracing + Zipkin 의존성 추가 (build.gradle) - user-service에 actuator 추가 - Config Server를 통해 모든 서비스에 Zipkin 설정 적용 - sampling.probability: 1.0 (100% 샘플링) - zipkin endpoint: http://zipkin:9411 - Docker Compose에 Zipkin 서버 추가 - API Gateway → User/Order Service 호출 추적 확인 - Order Service → User Service 호출 추적 확인 테스트 완료: - Zipkin UI (http://localhost:9411)에서 Trace 확인 - 서비스 간 호출 흐름 및 소요 시간 추적 성공 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
## 주요 구현 사항 ### 1. Order Service와 User Service 간 통신 추가 - OrderWithUserResponse DTO 생성 (Order + User 정보 통합) - UserClient (OpenFeign) 추가로 User Service 호출 - OrderController에 쿼리 파라미터 방식 API 추가 (GET /orders?userId=1) - Circuit Breaker fallback 추가 (User Service 장애 대응) ### 2. Micrometer Tracing + Zipkin 통합 - feign-micrometer 의존성 추가 (OpenFeign trace context 전파) - B3 propagation 설정 (Zipkin 호환) - Feign 로깅 설정 추가 (디버깅용) ### 3. 코드 개선 - OrderStatus enum을 별도 클래스로 분리 (재사용성 향상) - RESTful API 설계 (쿼리 파라미터 활용) ## 트러블슈팅 ### 문제 1: Order Service → User Service 호출이 Zipkin에서 연결되지 않음 - 증상: 각 서비스가 별도의 Trace ID 생성, Zipkin UI에서 분리된 trace로 표시 - 원인: Spring Boot 3.x + OpenFeign에서 trace context 헤더가 자동 전파되지 않음 - 해결: `io.github.openfeign:feign-micrometer` 의존성 추가 필수 ### 문제 2: Propagation format 미설정 - 증상: Trace 헤더가 전달되지 않음 - 해결: `management.tracing.propagation.type: b3` 설정 추가 ### 문제 3: Docker 빌드 캐싱 이슈 - 증상: 코드 변경사항이 Docker 컨테이너에 반영되지 않음 - 원인: IntelliJ 빌드 아티팩트가 Docker COPY 단계에서 복사됨 - 해결: `rm -rf order-service/build && docker-compose up -d --build` ## 테스트 결과 - ✅ Order Service → User Service 호출이 동일한 Trace ID로 연결 - ✅ Zipkin UI에서 3개의 span 확인 (order-service 2개, user-service 1개) - ✅ Circuit Breaker fallback 정상 동작 ## 참고 - Zipkin UI: http://localhost:9411 - API 예시: GET http://localhost:8082/orders?userId=1 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
WalkthroughLombok이 1.18.32로 상향되고 Micrometer Tracing(Zipkin)·Resilience4j·Feign 통합이 추가되었습니다. Zipkin 서비스가 docker-compose에 추가되고 Config Server의 서비스별 YAML에 추적·회로차단 설정이 반영되었으며 Order 서비스에 User 호출용 Feign 클라이언트, DTO, 서비스/컨트롤러 변경과 회로차단/타임리미터·로깅이 도입되었습니다. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor Client
participant GW as API Gateway
participant OS as Order Service
participant US as User Service
participant ZK as Zipkin
Note over GW,OS: B3 propagation, Micrometer Tracing enabled
Client->>GW: GET /orders?userId={id}
GW->>OS: Forward request (trace/span 전파)
alt userId present
OS->>OS: CircuitBreaker(userClient)
OS->>US: Feign GET /api/users/{id} (trace 전파)
US-->>OS: UserResponse
OS->>OS: 주문과 User 병합 (OrderWithUserResponse)
OS-->>GW: List<OrderWithUserResponse>
else no userId
OS->>OS: 모든 주문 조회
OS-->>GW: List<Order>
end
GW-->>Client: 200 OK
par Export spans
OS-->>ZK: Export spans
US-->>ZK: Export spans
GW-->>ZK: Export spans
end
rect rgba(255,230,230,0.4)
Note over OS: 오류 발생 시 CircuitBreaker -> fallback 제공
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
Pre-merge checks and finishing touches❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
order-service/src/main/java/com/example/order/service/OrderService.java (1)
48-62: Fallback에서 사용자 검증 없이 주문을 생성하면 데이터 무결성 문제가 발생할 수 있습니다.현재 fallback 메서드는 User Service 장애 시 사용자 검증 없이 주문을 생성합니다. 이는 존재하지 않는 사용자에 대한 주문이 생성될 수 있어 데이터 무결성 문제를 야기할 수 있습니다. 또한 호출자는 사용자 검증이 생략되었다는 사실을 알 수 없습니다.
다음 중 하나의 방법을 고려하세요:
옵션 1: 예외를 던져 호출자에게 실패를 명확히 알림 (권장)
private Order createOrderFallback(CreateOrderRequest request, Exception ex) { log.error("User Service 호출 실패! Circuit Breaker 작동 - userId: {}, error: {}", request.getUserId(), ex.getMessage()); - - // 사용자 검증 없이 주문 생성 (또는 예외를 던질 수도 있음) - log.warn("사용자 검증 없이 주문 생성 진행 - userId: {}", request.getUserId()); - - Order order = new Order( - request.getUserId(), - request.getProductName(), - request.getQuantity(), - request.getPrice() - ); - return orderRepository.save(order); + throw new ServiceUnavailableException("User Service를 사용할 수 없습니다. 잠시 후 다시 시도해주세요."); }옵션 2: 주문 상태를 "PENDING_VERIFICATION"으로 표시하고 나중에 검증
private Order createOrderFallback(CreateOrderRequest request, Exception ex) { log.error("User Service 호출 실패! Circuit Breaker 작동 - userId: {}, error: {}", request.getUserId(), ex.getMessage()); - - // 사용자 검증 없이 주문 생성 (또는 예외를 던질 수도 있음) - log.warn("사용자 검증 없이 주문 생성 진행 - userId: {}", request.getUserId()); - + + log.warn("사용자 검증 보류 상태로 주문 생성 - userId: {}", request.getUserId()); + Order order = new Order( request.getUserId(), request.getProductName(), request.getQuantity(), request.getPrice() ); + order.setStatus(OrderStatus.PENDING_VERIFICATION); // 검증 대기 상태 return orderRepository.save(order); }
🧹 Nitpick comments (8)
user-service/build.gradle (1)
2-2: Spring Boot 버전 업그레이드를 고려하세요.현재 Spring Boot 3.1.5를 사용하고 있는데, 이는 2023년 버전입니다. Spring Boot 3.4.x 이상으로 업그레이드하면 보안 패치와 성능 개선을 적용할 수 있습니다.
order-service/build.gradle (1)
2-2: Spring Boot 버전 업그레이드를 고려하세요.
user-service와 동일하게 Spring Boot 3.1.5를 사용 중입니다. 일관성 유지를 위해 전체 프로젝트의 Spring Boot 버전을 최신 버전(3.4.x)으로 통합 업그레이드하는 것을 권장합니다.config-server/src/main/resources/config/api-gateway.yml (1)
75-82: B3 propagation type 설정 추가를 고려하세요.
user-service.yml에는management.tracing.propagation.type: b3설정이 있는데,api-gateway.yml에는 없습니다. 일관성을 위해 동일한 propagation type을 명시하는 것을 권장합니다.다음과 같이 추가할 수 있습니다:
management: tracing: sampling: probability: 1.0 # 100% 샘플링 (개발 환경, 운영에서는 0.1 권장) + propagation: + type: b3 # Zipkin B3 propagation format zipkin: tracing: endpoint: http://zipkin:9411/api/v2/spansdocker-compose.yml (2)
48-56: 'latest' 태그 대신 특정 버전 사용을 고려하세요.Zipkin 이미지에
latest태그를 사용하고 있습니다. 재현 가능한 빌드를 위해 특정 버전(예:openzipkin/zipkin:3.4)을 명시하는 것을 권장합니다.zipkin: - image: openzipkin/zipkin:latest + image: openzipkin/zipkin:3.4 container_name: zipkin
55-56: 메모리 스토리지 사용에 유의하세요.
STORAGE_TYPE=mem은 개발 환경에 적합하지만, 컨테이너 재시작 시 모든 추적 데이터가 손실됩니다. 운영 환경에서는 Elasticsearch나 MySQL 등 영구 스토리지 사용을 고려하세요.order-service/src/main/java/com/example/order/config/FeignConfig.java (2)
7-13: 운영 환경에서는 로깅 레벨 조정을 고려하세요.
Logger.Level.FULL은 모든 요청/응답 헤더와 바디를 로깅합니다. 개발 환경에는 유용하지만, 운영 환경에서는 민감한 데이터(인증 토큰, 개인정보 등)가 로그에 노출될 수 있습니다. 프로파일별로 로깅 레벨을 다르게 설정하는 것을 권장합니다.프로파일별 설정 예시:
@Configuration public class FeignConfig { @Bean Logger.Level feignLoggerLevel(@Value("${spring.profiles.active:dev}") String profile) { return profile.contains("prod") ? Logger.Level.BASIC : Logger.Level.FULL; } }
11-11: 메서드 접근 제어자 명시를 고려하세요.Bean 메서드에 접근 제어자가 없습니다(package-private). Spring Boot best practice에 따라
public을 명시하는 것이 일반적입니다.@Bean - Logger.Level feignLoggerLevel() { + public Logger.Level feignLoggerLevel() { return Logger.Level.FULL; // FULL: 모든 요청/응답 헤더와 바디 로깅 }order-service/src/main/java/com/example/order/dto/OrderWithUserResponse.java (1)
13-17: 응답 DTO의 불변성을 고려하세요.현재
@Setter를 사용하여 응답 DTO가 변경 가능(mutable)하게 되어 있습니다. 응답 DTO는 일반적으로 불변(immutable)으로 설계하는 것이 권장됩니다. 이는 데이터 무결성을 보장하고 실수로 인한 변경을 방지합니다.다음과 같이
@Setter를 제거하고 필요시@Builder를 추가하는 것을 고려하세요:@Getter -@Setter +@Builder @NoArgsConstructor @AllArgsConstructor public class OrderWithUserResponse {또는 Java 14 이상을 사용하는 경우 record를 활용할 수도 있습니다:
public record OrderWithUserResponse( Long orderId, String productName, Integer quantity, BigDecimal price, OrderStatus status, LocalDateTime createdAt, Long userId, String userName, String userEmail ) { public static OrderWithUserResponse of(Order order, UserResponse user) { return new OrderWithUserResponse( order.getId(), order.getProductName(), order.getQuantity(), order.getPrice(), order.getStatus(), order.getCreatedAt(), user.getId(), user.getName(), user.getEmail() ); } }
📜 Review details
Configuration used: Path: .coderabbit.yml
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
docs/zipkin_screenshot.pngis excluded by!**/*.png,!**/docs/**
📒 Files selected for processing (17)
.idea/compiler.xml(1 hunks)build.gradle(1 hunks)config-server/src/main/resources/config/api-gateway.yml(2 hunks)config-server/src/main/resources/config/order-service.yml(2 hunks)config-server/src/main/resources/config/user-service.yml(2 hunks)docker-compose.yml(1 hunks)order-service/build.gradle(1 hunks)order-service/src/main/java/com/example/order/client/UserClient.java(1 hunks)order-service/src/main/java/com/example/order/config/FeignConfig.java(1 hunks)order-service/src/main/java/com/example/order/controller/OrderController.java(1 hunks)order-service/src/main/java/com/example/order/dto/OrderWithUserResponse.java(1 hunks)order-service/src/main/java/com/example/order/dto/UserResponse.java(1 hunks)order-service/src/main/java/com/example/order/entity/Order.java(1 hunks)order-service/src/main/java/com/example/order/entity/OrderStatus.java(1 hunks)order-service/src/main/java/com/example/order/service/OrderService.java(2 hunks)user-service/build.gradle(1 hunks)user-service/src/main/java/com/example/user/controller/UserController.java(2 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/build.gradle
⚙️ CodeRabbit configuration file
**/build.gradle: Review Gradle configuration for:
- Dependency management best practices
- Build optimization opportunities
- Plugin usage efficiency
Files:
user-service/build.gradleorder-service/build.gradlebuild.gradle
**/*.java
⚙️ CodeRabbit configuration file
**/*.java: Review this Java code for:
- Spring Boot best practices
- Clean code principles
- Performance optimizations
- Security considerations
- Suggest more elegant solutions using Java features
- Check for proper exception handling
- Suggest better naming conventions
Files:
order-service/src/main/java/com/example/order/entity/OrderStatus.javaorder-service/src/main/java/com/example/order/dto/UserResponse.javaorder-service/src/main/java/com/example/order/dto/OrderWithUserResponse.javaorder-service/src/main/java/com/example/order/client/UserClient.javaorder-service/src/main/java/com/example/order/controller/OrderController.javaorder-service/src/main/java/com/example/order/entity/Order.javauser-service/src/main/java/com/example/user/controller/UserController.javaorder-service/src/main/java/com/example/order/service/OrderService.javaorder-service/src/main/java/com/example/order/config/FeignConfig.java
🧬 Code graph analysis (3)
order-service/src/main/java/com/example/order/controller/OrderController.java (1)
order-service/src/main/java/com/example/order/service/OrderService.java (1)
Slf4j(19-122)
user-service/src/main/java/com/example/user/controller/UserController.java (2)
order-service/src/main/java/com/example/order/controller/OrderController.java (1)
Slf4j(14-44)order-service/src/main/java/com/example/order/service/OrderService.java (1)
Slf4j(19-122)
order-service/src/main/java/com/example/order/service/OrderService.java (3)
order-service/src/main/java/com/example/order/dto/OrderWithUserResponse.java (1)
Getter(13-44)order-service/src/main/java/com/example/order/controller/OrderController.java (1)
Slf4j(14-44)user-service/src/main/java/com/example/user/controller/UserController.java (1)
Slf4j(15-48)
🔇 Additional comments (12)
user-service/src/main/java/com/example/user/controller/UserController.java (2)
7-7: 로깅 인프라 추가 승인Lombok의
@Slf4j어노테이션을 통한 로거 추가가 적절합니다. OrderController와 일관된 패턴을 따르고 있으며, 분산 추적 구현 목적에 부합합니다.Also applies to: 15-15
37-37: 요청 추적 로그 승인API 호출 시작점을 로깅하는 것이 적절합니다. userId만 로깅하여 분산 추적 흐름을 파악하기에 충분하며, 민감정보 노출 없이 안전합니다.
.idea/compiler.xml (1)
9-9: LGTM!Lombok 버전이 1.18.32로 업데이트되었습니다. IDE 설정 파일로 기능적 변경사항은 없습니다.
order-service/build.gradle (1)
13-15: LGTM!Resilience4j Circuit Breaker, Actuator, Feign-Micrometer 의존성이 올바르게 추가되었습니다. 이는 분산 추적 및 장애 복구 기능 구현에 적합합니다.
config-server/src/main/resources/config/api-gateway.yml (1)
5-6: LGTM!
spring.application.name설정이 추가되어 Zipkin UI에서 서비스 식별이 명확해집니다.config-server/src/main/resources/config/user-service.yml (2)
5-6: LGTM!
spring.application.name설정이 추가되어 서비스 식별이 명확합니다.
39-48: LGTM!분산 추적 설정이 올바르게 구성되었습니다. B3 propagation format이 명시되어 있어 Zipkin과의 호환성이 보장됩니다. 운영 환경 배포 시 sampling probability를 0.1로 조정하는 것을 잊지 마세요.
order-service/src/main/java/com/example/order/entity/OrderStatus.java (1)
1-9: LGTM!
OrderStatusenum이 별도 클래스로 잘 분리되었습니다. 각 상태에 한글 주석이 추가되어 가독성이 좋습니다. Clean code 원칙에 부합하는 구조입니다.user-service/build.gradle (1)
12-12: Actuator 보안 설정 추가 필요
config-server/src/main/resources/config/user-service.yml에 Actuator 엔드포인트 노출 제한(management.endpoints.web.exposure.include,management.endpoints.web.exposure.exclude) 및 인증(management.security.*) 설정이 없습니다. 해당 파일에 필수 보안 설정을 추가하세요.order-service/src/main/java/com/example/order/service/OrderService.java (3)
27-45: LGTM! 사용자 검증 로직과 Circuit Breaker 적용이 적절합니다.분산 추적을 위한 User Service 호출과 Circuit Breaker 통합이 잘 구현되었습니다. 로깅도 적절하게 추가되어 있어 추적 가능성이 높습니다.
73-91: 메서드 구현이 깔끔하고 로깅도 적절합니다.사용자별 주문 조회와 User Service 호출을 통한 정보 통합 로직이 잘 구현되었습니다. Stream API를 효과적으로 사용하고 있으며, 로깅도 분산 추적에 적합하게 구성되어 있습니다.
25-25: 사용자 검증이 필요한 다른 메서드에서도 동일한 패턴을 적용하세요.UserClient가 추가되었으며 Circuit Breaker를 통해 안전하게 호출되고 있습니다. 향후 사용자 정보가 필요한 다른 비즈니스 로직에서도 동일한 패턴(Circuit Breaker + logging)을 일관되게 적용하는 것을 권장합니다.
| timelimiter: | ||
| configs: | ||
| default: | ||
| timeoutDuration: 3s |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TimeLimiter 설정이 userClient에 적용되지 않습니다.
timeoutDuration만 정의되어 있어서는 Feign 호출에 시간 제한이 걸리지 않습니다. resilience4j.timelimiter.instances.userClient를 선언하고 OrderService.getOrdersWithUserInfo 등 실제 호출부에 @TimeLimiter(name = "userClient")를 붙여야 3초 제한이 동작합니다. 그렇지 않으면 User Service가 응답 지연/정지 상태일 때 호출 스레드가 계속 대기해 회로 차단이나 fallback이 제때 작동하지 못합니다. 아래처럼 인스턴스를 추가해 주세요.
timelimiter:
configs:
default:
timeoutDuration: 3s
+ instances:
+ userClient:
+ baseConfig: default그리고 해당 메서드에 @TimeLimiter(name = "userClient")를 추가하는 것도 잊지 말아 주세요.
🤖 Prompt for AI Agents
config-server/src/main/resources/config/order-service.yml lines 67-70: 현재
timelimiter의 기본 config만 정의되어 있어 userClient Feign 호출에 시간 제한이 적용되지 않습니다; 추가로
resilience4j.timelimiter.instances.userClient 항목을 선언하고 timeoutDuration: 3s(또는
원하는 값)를 설정하여 인스턴스를 명시적으로 만들어 주세요, 그리고 OrderService.getOrdersWithUserInfo 등 해당
호출을 수행하는 메서드에 @TimeLimiter(name = "userClient") 어노테이션을 붙여 3초 제한이 실제로 적용되도록 하세요.
| public static OrderWithUserResponse of(Order order, UserResponse user) { | ||
| return new OrderWithUserResponse( | ||
| order.getId(), | ||
| order.getProductName(), | ||
| order.getQuantity(), | ||
| order.getPrice(), | ||
| order.getStatus(), | ||
| order.getCreatedAt(), | ||
| user.getId(), | ||
| user.getName(), | ||
| user.getEmail() | ||
| ); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
팩토리 메서드에 null 안전성을 추가하세요.
of 메서드가 null 매개변수를 처리하지 않아 order 또는 user가 null일 경우 NullPointerException이 발생합니다.
다음과 같이 null 검증을 추가하세요:
public static OrderWithUserResponse of(Order order, UserResponse user) {
+ if (order == null || user == null) {
+ throw new IllegalArgumentException("Order and User must not be null");
+ }
return new OrderWithUserResponse(
order.getId(),
order.getProductName(),
order.getQuantity(),
order.getPrice(),
order.getStatus(),
order.getCreatedAt(),
user.getId(),
user.getName(),
user.getEmail()
);
}또는 Java의 Objects.requireNonNull을 사용할 수도 있습니다:
+import java.util.Objects;
+
public static OrderWithUserResponse of(Order order, UserResponse user) {
+ Objects.requireNonNull(order, "Order must not be null");
+ Objects.requireNonNull(user, "User must not be null");
return new OrderWithUserResponse(
order.getId(),
order.getProductName(),
order.getQuantity(),
order.getPrice(),
order.getStatus(),
order.getCreatedAt(),
user.getId(),
user.getName(),
user.getEmail()
);
}🤖 Prompt for AI Agents
In order-service/src/main/java/com/example/order/dto/OrderWithUserResponse.java
around lines 31 to 43, the static factory method of(Order, UserResponse) lacks
null-safety and will throw a NullPointerException if order or user is null; add
explicit null checks at the start of the method (e.g., use
Objects.requireNonNull(order, "order must not be null") and
Objects.requireNonNull(user, "user must not be null") or equivalent if/throw
IllegalArgumentException) before accessing fields, ensuring clear error messages
and preventing ambiguous NPEs.
| private List<OrderWithUserResponse> getOrdersWithUserFallback(Long userId, Exception ex) { | ||
| log.error("User Service 호출 실패! Circuit Breaker 작동 - userId: {}, error: {}", | ||
| userId, ex.getMessage()); | ||
|
|
||
| // User 정보 없이 Order만 반환 (기본값) | ||
| List<Order> orders = orderRepository.findByUserId(userId); | ||
| return orders.stream() | ||
| .map(order -> OrderWithUserResponse.of(order, | ||
| new UserResponse(userId, "Unknown User", "[email protected]"))) | ||
| .collect(Collectors.toList()); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fallback에서 반환하는 가짜 사용자 데이터가 실제 데이터와 구분되지 않습니다.
현재 fallback은 "Unknown User"와 "[email protected]"을 기본값으로 사용하는데, 이는 실제로 그런 이름을 가진 사용자와 구분할 수 없습니다. 또한 호출자는 이것이 fallback 데이터인지 알 수 없습니다.
다음과 같이 개선하는 것을 권장합니다:
옵션 1: null 또는 Optional을 사용하여 사용자 정보가 없음을 명시 (권장)
OrderWithUserResponse를 수정하여 user 필드를 Optional로 변경하거나, 사용자 정보가 없을 때는 null을 반환:
private List<OrderWithUserResponse> getOrdersWithUserFallback(Long userId, Exception ex) {
log.error("User Service 호출 실패! Circuit Breaker 작동 - userId: {}, error: {}",
userId, ex.getMessage());
-
- // User 정보 없이 Order만 반환 (기본값)
+
+ log.warn("사용자 정보 없이 주문 목록만 반환 - userId: {}", userId);
+
List<Order> orders = orderRepository.findByUserId(userId);
return orders.stream()
.map(order -> OrderWithUserResponse.of(order,
- new UserResponse(userId, "Unknown User", "[email protected]")))
+ new UserResponse(userId, null, null))) // null로 사용자 정보 부재를 명시
.collect(Collectors.toList());
}옵션 2: 예외를 던져 호출자가 적절히 처리하도록 함
private List<OrderWithUserResponse> getOrdersWithUserFallback(Long userId, Exception ex) {
log.error("User Service 호출 실패! Circuit Breaker 작동 - userId: {}, error: {}",
userId, ex.getMessage());
-
- // User 정보 없이 Order만 반환 (기본값)
- List<Order> orders = orderRepository.findByUserId(userId);
- return orders.stream()
- .map(order -> OrderWithUserResponse.of(order,
- new UserResponse(userId, "Unknown User", "[email protected]")))
- .collect(Collectors.toList());
+ throw new ServiceUnavailableException("User Service를 사용할 수 없습니다. 잠시 후 다시 시도해주세요.");
}옵션 3: 별도의 응답 타입을 사용하여 fallback 상태를 명시
// 새로운 응답 타입
public class OrderListResponse {
private List<OrderWithUserResponse> orders;
private boolean userInfoAvailable;
private String message;
}🤖 Prompt for AI Agents
In order-service/src/main/java/com/example/order/service/OrderService.java
around lines 94–104, the fallback currently returns a fake UserResponse
("Unknown User", "[email protected]") which can be mistaken for real data;
change the fallback to clearly indicate missing user data (prefer Option 1):
update OrderWithUserResponse to allow the user field to be
Optional<UserResponse> or nullable, and in this fallback return orders with an
empty Optional or null user and, if available, set an explicit flag or message
on the response to indicate user info is unavailable; ensure calling code and
any serializers handle Optional/null properly and adjust method signatures/tests
accordingly.
| public ResponseEntity<User> getUserById(@PathVariable Long id) { | ||
| log.info("[User Controller] 사용자 조회 API 호출됨 - userId: {}", id); | ||
| User user = userService.getUserById(id); | ||
| log.info("[User Controller] 사용자 조회 완료 - userId: {}, userName: {}", user.getId(), user.getName()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
개인정보 로깅 제거 필요
user.getName()을 로그에 기록하는 것은 개인정보 보호 및 컴플라이언스 관점에서 문제가 될 수 있습니다. 로그는 Zipkin 및 로그 수집 시스템에 저장되어 GDPR/CCPA 같은 개인정보 보호 규정 위반 위험이 있습니다.
OrderController 패턴을 참고하여 식별자(userId)만 로깅하도록 수정하세요.
다음과 같이 수정하세요:
- log.info("[User Controller] 사용자 조회 완료 - userId: {}, userName: {}", user.getId(), user.getName());
+ log.info("[User Controller] 사용자 조회 완료 - userId: {}", user.getId());Null 안전성 검증 필요
user.getId() 및 user.getName() 호출 전에 null 체크가 없습니다. UserService.getUserById()가 null을 반환하거나, 사용자가 존재하지 않는 경우 적절한 예외 처리가 되는지 확인이 필요합니다.
다음 스크립트로 UserService 구현을 확인하세요:
🏁 Script executed:
#!/bin/bash
# UserService.getUserById() 구현 및 반환 타입 확인
rg -n -A 10 "getUserById.*Long" --type=java user-service/src/main/java/com/example/user/service/Length of output: 1117
개인정보 로깅 제거 필요
- log.info("[User Controller] 사용자 조회 완료 - userId: {}, userName: {}", user.getId(), user.getName());
+ log.info("[User Controller] 사용자 조회 완료 - userId: {}", user.getId());Null 안전성 검증
UserService.getUserById()는 .orElseThrow(...)로 null을 반환하지 않으므로 별도 null 체크 불필요.
🤖 Prompt for AI Agents
In user-service/src/main/java/com/example/user/controller/UserController.java
around line 39, the log currently emits personal data (userName) and there is an
unnecessary null-safety check after calling UserService.getUserById(); remove
logging of personal information by eliminating or anonymizing user.getName()
from the log (keep non-sensitive context such as userId or requestId only), and
delete the redundant null check following getUserById() since it already throws
on absent users.
- 핵심 개념: Trace, Span, Context Propagation - 구현 가이드: Spring Boot 3.x + OpenFeign + Micrometer - 트러블슈팅: feign-micrometer 필수, propagation 설정 - 면접 Q&A: Zipkin vs Prometheus, 서비스 간 통신 패턴 - API Gateway vs Service-to-Service 직접 호출 설명 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
- 마크다운 테이블 대신 명확한 코드 블록으로 변경 - B3 Multi/Single 헤더 형식 예시 추가 - W3C traceparent 헤더 형식 예시 추가 - 선택 기준 명확화 (기존 시스템 vs 새 프로젝트) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
- 기술 스택 섹션 추가 (Java 21, Spring Boot 3.1.5, Micrometer Tracing) - OpenFeign 서비스 간 통신 명시 - 분산 추적 (Zipkin) 상세 설명 추가 - 최근 개선사항 섹션 추가 (Order-User 통신 구현) - 기술적 도전 과제 및 해결 방법 문서화 - Docker Compose 실행 가이드 개선 - API 테스트 예시 및 접속 URL 추가 - 관련 문서 링크 추가 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
- resilience4j.timelimiter.instances.userClient 인스턴스 선언 - OrderService의 createOrder와 getOrdersWithUserInfo 메서드에 @Timelimiter 어노테이션 추가 - User Service 호출 시 3초 타임아웃 제한이 정상 동작하도록 개선 - 타임아웃 발생 시 Circuit Breaker와 Fallback이 제때 작동 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
order-service/src/main/java/com/example/order/service/OrderService.java (1)
49-64: Fallback에서 사용자 검증을 우회하는 것은 보안상 위험합니다.createOrderFallback은 사용자 검증 없이 주문을 생성하는데, 이는 존재하지 않는 사용자의 주문 생성을 허용할 수 있습니다. User Service 장애 시에도 데이터 무결성이 유지되어야 합니다.
다음 중 하나를 선택하세요:
옵션 1: 예외를 던져 주문 생성을 차단 (권장)
private Order createOrderFallback(CreateOrderRequest request, Exception ex) { log.error("User Service 호출 실패! Circuit Breaker 작동 - userId: {}, error: {}", request.getUserId(), ex.getMessage()); - - // 사용자 검증 없이 주문 생성 (또는 예외를 던질 수도 있음) - log.warn("사용자 검증 없이 주문 생성 진행 - userId: {}", request.getUserId()); - - Order order = new Order( - request.getUserId(), - request.getProductName(), - request.getQuantity(), - request.getPrice() - ); - return orderRepository.save(order); + throw new ServiceUnavailableException("사용자 검증 서비스를 사용할 수 없습니다. 잠시 후 다시 시도해주세요."); }옵션 2: 캐시된 사용자 정보로 검증 (복잡하지만 가용성 향상)
로컬 캐시나 Redis에서 사용자 정보를 조회하여 검증한 후 주문 생성:
private Order createOrderFallback(CreateOrderRequest request, Exception ex) { log.error("User Service 호출 실패! Circuit Breaker 작동 - userId: {}, error: {}", request.getUserId(), ex.getMessage()); // 캐시에서 사용자 검증 시도 if (!userCache.exists(request.getUserId())) { throw new ServiceUnavailableException("사용자 검증 서비스를 사용할 수 없습니다."); } Order order = new Order( request.getUserId(), request.getProductName(), request.getQuantity(), request.getPrice() ); return orderRepository.save(order); }
♻️ Duplicate comments (2)
config-server/src/main/resources/config/order-service.yml (1)
67-73: TimeLimiter 인스턴스 추가로 이전 지적사항이 해소되었습니다.userClient 인스턴스가 선언되어 실제 @Timelimiter(name="userClient")에 적용 가능합니다. 이전 리뷰의 경고가 해결된 것으로 보입니다.
order-service/src/main/java/com/example/order/service/OrderService.java (1)
97-107: [중복] Fallback에서 반환하는 가짜 사용자 데이터가 실제 데이터와 구분되지 않습니다.이 문제는 이전 리뷰에서 이미 지적되었습니다. "Unknown User"와 "[email protected]"을 기본값으로 사용하면 실제로 그런 이름을 가진 사용자와 구분할 수 없으며, 호출자는 이것이 fallback 데이터인지 알 수 없습니다.
이전 리뷰의 옵션 1(null 또는 Optional 사용)을 우선적으로 고려해주세요.
🧹 Nitpick comments (5)
config-server/src/main/resources/config/order-service.yml (4)
5-6: 원격 설정에서 spring.application.name은 불필요합니다.Config Server의 order-service.yml은 이미 애플리케이션 이름과 매칭되어 로드됩니다. 여기서 application.name을 지정해도 부트스트랩 단계에는 영향이 없고 혼란만 줄 수 있으니 제거를 권장합니다.
32-33: Feign/클라이언트 DEBUG 로그는 프로필로 제한하세요.DEBUG는 운영에서 과도합니다. application-dev.yml 등 프로필별로 분리하거나 운영에서는 INFO로 낮추세요. Feign 상세 요청/응답 로깅은 Logger.Level(FULL) 설정이 별도로 필요하니 중복 설정 여부도 점검 바랍니다.
41-51: Zipkin/Tracing 설정 적절합니다. 운영에서는 샘플링률 낮추는 것을 권장합니다.개발 환경에서 probability 1.0은 OK. 운영 프로필로는 0.1 등 낮은 값으로 분리하세요. endpoint는 docker 네트워크용(zipkin 호스트)으로 보이니 로컬/운영별로 분리 또는 환경변수 치환(${ZIPKIN_BASE_URL}) 사용을 제안합니다.
54-66: CircuitBreaker 설정: 가독성을 위해 시간 단위 명시 권장waitDurationInOpenState: 10000은 ms로 해석되지만, ‘10s’처럼 단위를 명시하면 오해를 줄일 수 있습니다. 필요 시 slowCallDurationThreshold/slowCallRateThreshold도 함께 정의해 느린 호출 실패 비율 기반 보호를 강화할 수 있습니다.
order-service/src/main/java/com/example/order/service/OrderService.java (1)
79-84: 주문이 없을 때 불필요한 User Service 호출을 방지하세요.주문 목록이 비어있으면 User Service를 호출할 필요가 없습니다. 이는 불필요한 네트워크 오버헤드를 줄이고 User Service 부하를 감소시킵니다.
다음과 같이 개선하세요:
// 1. 해당 사용자의 주문 목록 조회 List<Order> orders = orderRepository.findByUserId(userId); log.info("[Order Service] 주문 목록 조회 완료 - userId: {}, orderCount: {}", userId, orders.size()); + +// 주문이 없으면 빈 리스트 반환 +if (orders.isEmpty()) { + log.info("[Order Service] 주문 목록이 비어있음 - User Service 호출 스킵"); + return List.of(); +} // 2. User Service 호출하여 사용자 정보 조회 UserResponse user = userClient.getUserById(userId);
📜 Review details
Configuration used: Path: .coderabbit.yml
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (2)
README.mdis excluded by!**/*.mddocs/Zipkin-Distributed-Tracing.mdis excluded by!**/*.md,!**/docs/**
📒 Files selected for processing (2)
config-server/src/main/resources/config/order-service.yml(2 hunks)order-service/src/main/java/com/example/order/service/OrderService.java(2 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.java
⚙️ CodeRabbit configuration file
**/*.java: Review this Java code for:
- Spring Boot best practices
- Clean code principles
- Performance optimizations
- Security considerations
- Suggest more elegant solutions using Java features
- Check for proper exception handling
- Suggest better naming conventions
Files:
order-service/src/main/java/com/example/order/service/OrderService.java
🧬 Code graph analysis (1)
order-service/src/main/java/com/example/order/service/OrderService.java (2)
order-service/src/main/java/com/example/order/dto/OrderWithUserResponse.java (1)
Getter(13-44)order-service/src/main/java/com/example/order/controller/OrderController.java (1)
Slf4j(14-44)
🔇 Additional comments (2)
order-service/src/main/java/com/example/order/service/OrderService.java (2)
3-26: 의존성 주입 및 로깅 설정이 적절합니다.UserClient 의존성 주입과 Lombok @slf4j 사용이 Spring Boot 모범 사례를 따릅니다.
28-29: CircuitBreaker 및 TimeLimiter 인스턴스 설정 확인
resilience4j.circuitbreaker.instances.userClient및resilience4j.timelimiter.instances.userClient항목이 application.yml 또는 Config Server에 정의되어 있는지 확인하세요. 정의되어 있지 않다면 failureRateThreshold, waitDurationInOpenState, timeoutDuration 등 필요한 설정을 추가해야 합니다.
| @CircuitBreaker(name = "userClient", fallbackMethod = "createOrderFallback") | ||
| @TimeLimiter(name = "userClient") | ||
| public Order createOrder(CreateOrderRequest request) { | ||
| // 분산 추적 테스트: User Service 호출하여 사용자 검증 | ||
| log.info("주문 생성 요청 - userId: {}, productName: {}", request.getUserId(), request.getProductName()); | ||
|
|
||
| UserResponse user = userClient.getUserById(request.getUserId()); | ||
| log.info("사용자 검증 완료 - userId: {}, userName: {}", user.getId(), user.getName()); | ||
|
|
||
| Order order = new Order( | ||
| request.getUserId(), | ||
| request.getProductName(), | ||
| request.getQuantity(), | ||
| request.getPrice() | ||
| ); | ||
| Order savedOrder = orderRepository.save(order); | ||
| log.info("주문 생성 완료 - orderId: {}", savedOrder.getId()); | ||
|
|
||
| return savedOrder; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
사용자 응답에 대한 null 안전성 검사가 필요합니다.
Line 34의 userClient.getUserById() 호출 결과가 null일 경우 NPE가 발생할 수 있습니다. Feign 클라이언트가 404나 오류 시 null을 반환하거나 예외를 던질 수 있으므로 명시적인 검증이 필요합니다.
다음과 같이 개선하세요:
UserResponse user = userClient.getUserById(request.getUserId());
+if (user == null) {
+ throw new IllegalArgumentException("사용자를 찾을 수 없습니다 - userId: " + request.getUserId());
+}
log.info("사용자 검증 완료 - userId: {}, userName: {}", user.getId(), user.getName());📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| @CircuitBreaker(name = "userClient", fallbackMethod = "createOrderFallback") | |
| @TimeLimiter(name = "userClient") | |
| public Order createOrder(CreateOrderRequest request) { | |
| // 분산 추적 테스트: User Service 호출하여 사용자 검증 | |
| log.info("주문 생성 요청 - userId: {}, productName: {}", request.getUserId(), request.getProductName()); | |
| UserResponse user = userClient.getUserById(request.getUserId()); | |
| log.info("사용자 검증 완료 - userId: {}, userName: {}", user.getId(), user.getName()); | |
| Order order = new Order( | |
| request.getUserId(), | |
| request.getProductName(), | |
| request.getQuantity(), | |
| request.getPrice() | |
| ); | |
| Order savedOrder = orderRepository.save(order); | |
| log.info("주문 생성 완료 - orderId: {}", savedOrder.getId()); | |
| return savedOrder; | |
| } | |
| @CircuitBreaker(name = "userClient", fallbackMethod = "createOrderFallback") | |
| @TimeLimiter(name = "userClient") | |
| public Order createOrder(CreateOrderRequest request) { | |
| // 분산 추적 테스트: User Service 호출하여 사용자 검증 | |
| log.info("주문 생성 요청 - userId: {}, productName: {}", request.getUserId(), request.getProductName()); | |
| UserResponse user = userClient.getUserById(request.getUserId()); | |
| if (user == null) { | |
| throw new IllegalArgumentException("사용자를 찾을 수 없습니다 - userId: " + request.getUserId()); | |
| } | |
| log.info("사용자 검증 완료 - userId: {}, userName: {}", user.getId(), user.getName()); | |
| Order order = new Order( | |
| request.getUserId(), | |
| request.getProductName(), | |
| request.getQuantity(), | |
| request.getPrice() | |
| ); | |
| Order savedOrder = orderRepository.save(order); | |
| log.info("주문 생성 완료 - orderId: {}", savedOrder.getId()); | |
| return savedOrder; | |
| } |
🤖 Prompt for AI Agents
In order-service/src/main/java/com/example/order/service/OrderService.java
around lines 28 to 47, add a null-safety check after the
userClient.getUserById(...) call: if the returned UserResponse is null, log a
clear message with the requested userId and throw a specific runtime exception
(e.g., UserNotFoundException or IllegalStateException) so we avoid an NPE later;
ensure the CircuitBreaker/TimeLimiter fallback method signature still matches
(adjust createOrderFallback if it needs to handle the new exception type) and
keep the existing logging for successful validation.
- 단일 메서드에서 if 분기하던 것을 두 개의 명시적인 엔드포인트로 분리
- ResponseEntity<?> 와일드카드 타입을 구체적인 타입으로 변경
- GET /orders: 전체 주문 조회 (List<Order>)
- GET /orders?userId={id}: 특정 사용자 주문 조회 (List<OrderWithUserResponse>)
- 타입 안정성 향상 및 API 문서화 개선
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <[email protected]>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
📜 Review details
Configuration used: Path: .coderabbit.yml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
order-service/src/main/java/com/example/order/controller/OrderController.java(1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.java
⚙️ CodeRabbit configuration file
**/*.java: Review this Java code for:
- Spring Boot best practices
- Clean code principles
- Performance optimizations
- Security considerations
- Suggest more elegant solutions using Java features
- Check for proper exception handling
- Suggest better naming conventions
Files:
order-service/src/main/java/com/example/order/controller/OrderController.java
🧬 Code graph analysis (1)
order-service/src/main/java/com/example/order/controller/OrderController.java (1)
order-service/src/main/java/com/example/order/service/OrderService.java (1)
Slf4j(20-125)
🔇 Additional comments (2)
order-service/src/main/java/com/example/order/controller/OrderController.java (2)
8-8: 로깅 구현이 잘 되어 있습니다.Lombok의
@Slf4j를 활용한 로깅이 적절히 추가되었으며, 각 주요 작업(주문 생성, 조회)의 시작과 완료를 추적하고 있습니다. 이는 분산 추적 환경에서 문제 진단에 유용합니다.Also applies to: 14-14, 24-24, 26-26, 32-32, 39-39
16-16: 게이트웨이 RewritePath 필터로/api/orders/**요청이/orders/**로 매핑되므로 기존 클라이언트 호환성이 유지됩니다.
📝 Summary
Micrometer Tracing + Zipkin을 통한 마이크로서비스 분산 추적 시스템을 구현했습니다.
Order Service와 User Service 간 서비스 호출을 추적하여 Zipkin UI에서 전체 요청 흐름을 시각화할 수 있습니다.
Closes #10
🚀 주요 구현 사항
1. Order Service와 User Service 간 통신 추가
GET /orders?userId=1)2. Micrometer Tracing + Zipkin 통합
3. 코드 개선
🔧 트러블슈팅
Issue 1: Order Service → User Service 호출이 Zipkin에서 연결되지 않음
증상
원인
Spring Boot 3.x + Spring Cloud 2022.0.x에서 OpenFeign 호출 시 trace context (Trace ID, Span ID)가 HTTP 헤더로 자동 전파되지 않음.
해결 방법
implementation 'io.github.openfeign:feign-micrometer'이렇게 하면 OpenFeign 호출 시 자동으로
X-B3-TraceId,X-B3-SpanId,X-B3-ParentSpanId헤더가 전달됩니다.결과
Issue 2: Docker 빌드 캐싱 이슈
증상
docker-compose up -d --build실행해도 코드 변경사항이 반영되지 않음.원인
Dockerfile의
COPY . .명령이 IntelliJ에서 미리 빌드된 JAR 파일을 포함한 전체 프로젝트를 복사하여, Docker의 Gradle 빌드가 캐시된 아티팩트를 사용함.해결 방법
rm -rf order-service/build && docker-compose up -d --build order-service빌드 디렉토리를 삭제한 후 Docker 빌드를 수행하여 새로운 JAR 파일 생성.
Issue 3: Eureka 등록 타이밍 이슈
증상
Order Service 시작 직후 User Service 호출 시 Circuit Breaker fallback 실행.
원인
User Service가 Eureka에 등록되기 전에 Order Service가 호출을 시도함.
해결 방법
📊 테스트 결과
Zipkin Trace 확인
결과:
{ "traceId": "68eb6934747c42b7237d7d4de82a0276", "totalSpans": 3, "services": ["order-service", "user-service"], "spans": [ { "name": "http get /orders", "service": "order-service", "duration": 19094 }, { "name": "http get", "service": "order-service", "duration": 5486 }, { "name": "http get /api/users/{id}", "service": "user-service", "duration": 3576 } ] }✅ 3개의 span이 동일한 Trace ID로 연결됨!
API 테스트
응답:
[ { "orderId": 1, "productName": "Laptop", "quantity": 2, "price": 1500000.00, "status": "PENDING", "createdAt": "2025-10-12T07:26:35.985652", "userId": 1, "userName": "Test User", "userEmail": "[email protected]" } ]✅ Order + User 정보가 통합되어 반환됨!
🎯 학습 포인트
1. Spring Boot 3.x에서의 분산 추적
feign-micrometer필수2. MSA 데이터 통합 패턴
3. Docker 빌드 최적화
📸 Zipkin UI 스크린샷
Zipkin UI (http://localhost:9411)에서 확인 가능:
🔗 관련 리소스
📋 체크리스트
🤖 Generated with Claude Code
Summary by CodeRabbit